/*
 * Zmanim Java API
 * Copyright (C) 2004-2010 Eliyahu Hershfeld
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if
 * not, write to the Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA or connect to: http://www.fsf.org/copyleft/gpl.html
 */
package net.sourceforge.zmanim.util;

import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Calendar;
import java.util.List;
import java.text.SimpleDateFormat;
import net.sourceforge.zmanim.*;


/**
 * A class used to format non {@link java.util.Date} times generated by the
 * Zmanim package. For example the
 * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour()} returns
 * the length of the hour in milliseconds. This class can format this time.
 * 
 * @author &copy; Eliyahu Hershfeld 2004 - 2010
 * @version 1.2
 */
public class ZmanimFormatter {
	private boolean prependZeroHours;

	private boolean useSeconds;

	private boolean useMillis;

	boolean useDecimal;

	private static DecimalFormat minuteSecondNF = new DecimalFormat("00");

	private DecimalFormat hourNF;

	private static DecimalFormat milliNF = new DecimalFormat("000");

	private SimpleDateFormat dateFormat;

	// private DecimalFormat decimalNF;

	/**
	 * Format using hours, minutes, seconds and milliseconds using the xsd:time
	 * format. This format will return 00.00.00.0 when formatting 0.
	 */
	public static final int SEXAGESIMAL_XSD_FORMAT = 0;

	private int timeFormat = SEXAGESIMAL_XSD_FORMAT;

	/**
	 * Format using standard decimal format with 5 positions after the decimal.
	 */
	public static final int DECIMAL_FORMAT = 1;

	/** Format using hours and minutes. */
	public static final int SEXAGESIMAL_FORMAT = 2;

	/** Format using hours, minutes and seconds. */
	public static final int SEXAGESIMAL_SECONDS_FORMAT = 3;

	/** Format using hours, minutes, seconds and milliseconds. */
	public static final int SEXAGESIMAL_MILLIS_FORMAT = 4;
	
	/** constant for milliseconds in a minute (60,000) */
	static final long MINUTE_MILLIS = 60 * 1000;
	
	/** constant for milliseconds in an hour (3,600,000) */
	public static final long HOUR_MILLIS = MINUTE_MILLIS * 60;

	/**
	 * Format using the XSD Duration format. This is in the format of
	 * PT1H6M7.869S (P for period (duration), T for time, H, M and S indicate
	 * hours, minutes and seconds.
	 */
	public static final int XSD_DURATION_FORMAT = 5;

	public ZmanimFormatter() {
		this(0, new SimpleDateFormat("h:mm:ss"));
	}

	/**
	 * ZmanimFormatter constructor using a formatter
	 * 
	 * @param format
	 *            int The formatting style to use. Using
	 *            ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the
	 *            time time of 90*60*1000 + 1 as 1:30:00
	 */
	public ZmanimFormatter(int format, SimpleDateFormat dateFormat) {
		String hourFormat = "0";
		if (prependZeroHours) {
			hourFormat = "00";
		}
		hourNF = new DecimalFormat(hourFormat);
		// decimalNF = new DecimalFormat("0.0####");
		setTimeFormat(format);
		this.setDateFormat(dateFormat);
	}

	/**
	 * Sets the format to use for formatting.
	 * 
	 * @param format
	 *            int the format constant to use.
	 */
	public void setTimeFormat(int format) {
		timeFormat = format;
		switch (format) {
		case SEXAGESIMAL_XSD_FORMAT:
			setSettings(true, true, true);
			break;
		case SEXAGESIMAL_FORMAT:
			setSettings(false, false, false);
			break;
		case SEXAGESIMAL_SECONDS_FORMAT:
			setSettings(false, true, false);
			break;
		case SEXAGESIMAL_MILLIS_FORMAT:
			setSettings(false, true, true);
			break;
		case DECIMAL_FORMAT:
		default:
			useDecimal = true;
		}
	}

	public void setDateFormat(SimpleDateFormat sdf) {
		dateFormat = sdf;
	}

	public SimpleDateFormat getDateFormat() {
		return dateFormat;
	}

	private void setSettings(boolean prependZeroHours, boolean useSeconds,
			boolean useMillis) {
		this.prependZeroHours = prependZeroHours;
		this.useSeconds = useSeconds;
		this.useMillis = useMillis;
	}

	/**
	 * A method that formats milliseconds into a time format.
	 * 
	 * @param milliseconds
	 *            The time in milliseconds.
	 * @return String The formatted <code>String</code>
	 */
	public String format(double milliseconds) {
		return format((int) milliseconds);
	}

	/**
	 * A method that formats milliseconds into a time format.
	 * 
	 * @param millis
	 *            The time in milliseconds.
	 * @return String The formatted <code>String</code>
	 */
	public String format(int millis) {
		return format(new Time(millis));
	}

	/**
	 * A method that formats {@link Time}objects.
	 * 
	 * @param time
	 *            The time <code>Object</code> to be formatted.
	 * @return String The formatted <code>String</code>
	 */
	public String format(Time time) {
		if (timeFormat == XSD_DURATION_FORMAT) {
			return formatXSDDurationTime(time);
		}
		StringBuffer sb = new StringBuffer();
		sb.append(hourNF.format(time.getHours()));
		sb.append(":");
		sb.append(minuteSecondNF.format(time.getMinutes()));
		if (useSeconds) {
			sb.append(":");
			sb.append(minuteSecondNF.format(time.getSeconds()));
		}
		if (useMillis) {
			sb.append(".");
			sb.append(milliNF.format(time.getMilliseconds()));
		}
		return sb.toString();
	}

	/**
	 * Formats a date using this classe's {@link #getDateFormat() date format}.
	 * 
	 * @param dateTime
	 *            the date to format
	 * @param calendar
	 *            the {@link java.util.Calendar Calendar} used to help format
	 *            based on the Calendar's DST and other settings.
	 * @return the formatted String
	 */
	public String formatDateTime(Date dateTime, Calendar calendar) {
		dateFormat.setCalendar(calendar);
		if (this.dateFormat.toPattern().equals("yyyy-MM-dd'T'HH:mm:ss")) {
			return getXSDateTime(dateTime, calendar);
		} else {
			return dateFormat.format(dateTime);
		}

	}

	/**
	 * The date:date-time function returns the current date and time as a
	 * date/time string. The date/time string that's returned must be a string
	 * in the format defined as the lexical representation of xs:dateTime in <a
	 * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">[3.3.8 dateTime]</a>
	 * of <a href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
	 * Datatypes]</a>. The date/time format is basically CCYY-MM-DDThh:mm:ss,
	 * although implementers should consult <a
	 * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
	 * Datatypes]</a> and <a href="http://www.iso.ch/markete/8601.pdf">[ISO
	 * 8601]</a> for details. The date/time string format must include a time
	 * zone, either a Z to indicate Coordinated Universal Time or a + or -
	 * followed by the difference between the difference from UTC represented as
	 * hh:mm.
	 */
	public String getXSDateTime(Date dateTime, Calendar cal) {
		String xsdDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss";
		/*
		 * if (xmlDateFormat == null || xmlDateFormat.trim().equals("")) {
		 * xmlDateFormat = xsdDateTimeFormat; }
		 */
		SimpleDateFormat dateFormat = new SimpleDateFormat(xsdDateTimeFormat);

		StringBuffer buff = new StringBuffer(dateFormat.format(dateTime));
		// Must also include offset from UTF.
		// Get the offset (in milliseconds).
		int offset = cal.get(Calendar.ZONE_OFFSET)
				+ cal.get(Calendar.DST_OFFSET);
		// If there is no offset, we have "Coordinated
		// Universal Time."
		if (offset == 0)
			buff.append("Z");
		else {
			// Convert milliseconds to hours and minutes
			int hrs = offset / (60 * 60 * 1000);
			// In a few cases, the time zone may be +/-hh:30.
			int min = offset % (60 * 60 * 1000);
			char posneg = hrs < 0 ? '-' : '+';
			buff.append(posneg + formatDigits(hrs) + ':' + formatDigits(min));
		}
		return buff.toString();
	}

	/**
	 * Represent the hours and minutes with two-digit strings.
	 * 
	 * @param digits hours or minutes.
	 * @return two-digit String representation of hrs or minutes.
	 */
	private static String formatDigits(int digits) {
		String dd = String.valueOf(Math.abs(digits));
		return dd.length() == 1 ? '0' + dd : dd;
	}

	/**
	 * This returns the xml representation of an xsd:duration object.
	 * 
	 * @param millis the duration in milliseconds
	 * @return the xsd:duration formatted String
	 */
	public String formatXSDDurationTime(long millis) {
		return formatXSDDurationTime(new Time(millis));
	}

	/**
	 * This returns the xml representation of an xsd:duration object.
	 * 
	 * @param time the duration as a Time object 
	 * @return the xsd:duration formatted String
	 */
	public String formatXSDDurationTime(Time time) {
		StringBuffer duration = new StringBuffer();

		duration.append("P");

		if (time.getHours() != 0 || time.getMinutes() != 0
				|| time.getSeconds() != 0 || time.getMilliseconds() != 0) {
			duration.append("T");

			if (time.getHours() != 0)
				duration.append(time.getHours() + "H");

			if (time.getMinutes() != 0)
				duration.append(time.getMinutes() + "M");

			if (time.getSeconds() != 0 || time.getMilliseconds() != 0) {
				duration.append(time.getSeconds() + "."
						+ milliNF.format(time.getMilliseconds()));
				duration.append("S");
			}
			if (duration.length() == 1) // zero seconds
				duration.append("T0S");
			if (time.isNegative())
				duration.insert(0, "-");
		}
		return duration.toString();
	}
	
	/**
	 * A method that returns an XML formatted <code>String</code> representing
	 * the serialized <code>Object</code>. The format used is:
	 * 
	 * <pre>
	 *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
	 *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
	 *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
	 *     ...
	 *   &lt;/AstronomicalTimes&gt;
	 * </pre>
	 * 
	 * Note that the output uses the <a
	 * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a>
	 * format for times such as sunrise, and <a
	 * href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a>
	 * format for times that are a duration such as the length of a
	 * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
	 * returned by the {@link #toString() toString} }.
	 * 
	 * @return The XML formatted <code>String</code>. The format will be:
	 * 
	 * <pre>
	 *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
	 *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
	 *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
	 *     ...
	 *  &lt;/AstronomicalTimes&gt;
	 * </pre>
	 * 
	 */
	public static String toXML(AstronomicalCalendar ac) {
		ZmanimFormatter formatter = new ZmanimFormatter(
				ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
						"yyyy-MM-dd'T'HH:mm:ss"));
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
		String output = "<";
		if (ac.getClass().getName().endsWith("AstronomicalCalendar")) {
			output += "AstronomicalTimes";
		} else if (ac.getClass().getName().endsWith("ZmanimCalendar")) {
			output += "Zmanim";
		}
		output += " date=\"" + df.format(ac.getCalendar().getTime()) + "\"";
		output += " type=\"" + ac.getClass().getName() + "\"";
		output += " algorithm=\""
				+ ac.getAstronomicalCalculator().getCalculatorName() + "\"";
		output += " location=\"" + ac.getGeoLocation().getLocationName() + "\"";
		output += " latitude=\"" + ac.getGeoLocation().getLatitude() + "\"";
		output += " longitude=\"" + ac.getGeoLocation().getLongitude() + "\"";
		output += " elevation=\"" + ac.getGeoLocation().getElevation() + "\"";
		output += " timeZoneName=\""
				+ ac.getGeoLocation().getTimeZone().getDisplayName() + "\"";
		output += " timeZoneID=\"" + ac.getGeoLocation().getTimeZone().getID()
				+ "\"";
		output += " timeZoneOffset=\""
				+ (ac.getGeoLocation().getTimeZone().getOffset(
						ac.getCalendar().getTimeInMillis()) / ((double)HOUR_MILLIS))
				+ "\"";

		output += ">\n";

		Method[] theMethods = ac.getClass().getMethods();
		String tagName = "";
		Object value = null;
		List dateList = new ArrayList();
		List durationList = new ArrayList();
		List otherList = new ArrayList();
		for (int i = 0; i < theMethods.length; i++) {
			if (includeMethod(theMethods[i])) {
				tagName = theMethods[i].getName().substring(3);
				//String returnType = theMethods[i].getReturnType().getName();
				try {
					value = theMethods[i].invoke(ac, (Object[]) null);
					if (value == null) {//FIXME: use reflection to determine what the return type is, not the value
						otherList.add("<" + tagName + ">N/A</" + tagName + ">");
					} else if (value instanceof Date) {
						dateList.add(new Zman((Date) value, tagName));
					} else if (value instanceof Long) {// shaah zmanis
						durationList.add(new Zman((int) ((Long) value)
								.longValue(), tagName));
					} else { // will probably never enter this block, but is
						// present to be future proof
						otherList.add("<" + tagName + ">" + value + "</"
								+ tagName + ">");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		Zman zman;
		Collections.sort(dateList, Zman.DATE_ORDER);
		for (int i = 0; i < dateList.size(); i++) {
			zman = (Zman) dateList.get(i);
			output += "\t<" + zman.getZmanLabel();
			output += ">";
			output += formatter.formatDateTime(zman.getZman(), ac.getCalendar())
					+ "</" + zman.getZmanLabel() + ">\n";
		}
		Collections.sort(durationList, Zman.DURATION_ORDER);
		for (int i = 0; i < durationList.size(); i++) {
			zman = (Zman) durationList.get(i);
			output += "\t<" + zman.getZmanLabel();
			output += ">";
			output += formatter.format((int) zman.getDuration()) + "</"
					+ zman.getZmanLabel() + ">\n";
		}

		for (int i = 0; i < otherList.size(); i++) {// will probably never enter
			// this block
			output += "\t" + otherList.get(i) + "\n";
		}

		if (ac.getClass().getName().endsWith("AstronomicalCalendar")) {
			output += "</AstronomicalTimes>";
		} else if (ac.getClass().getName().endsWith("ZmanimCalendar")) {
			output += "</Zmanim>";
		}
		return output;
	}
	
	/**
	 * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)}
	 * 
	 * @param method
	 * @return
	 */
	private static boolean includeMethod(Method method) {
		List methodWhiteList = new ArrayList();
		// methodWhiteList.add("getName");

		List methodBlackList = new ArrayList();
		// methodBlackList.add("getGregorianChange");

		if (methodWhiteList.contains(method.getName()))
			return true;
		if (methodBlackList.contains(method.getName()))
			return false;

		if (method.getParameterTypes().length > 0)
			return false; // Skip get methods with parameters since we do not
		// know what value to pass
		if (!method.getName().startsWith("get"))
			return false;

		if (method.getReturnType().getName().endsWith("Date")
				|| method.getReturnType().getName().endsWith("long")) {
			return true;
		}
		return false;
	}
}